package io.focus.mijin.zuul.ratelimit.filters;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import io.focus.mijin.zuul.ratelimit.config.RateLimitKeyGenerator;
import io.focus.mijin.zuul.ratelimit.config.properties.RateLimitProperties;
import io.focus.mijin.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
import io.focus.mijin.zuul.ratelimit.config.properties.RateLimitProperties.Policy.MatchType;
import io.focus.mijin.zuul.ratelimit.config.properties.RateLimitProperties.Policy.Type;
import io.focus.mijin.zuul.ratelimit.support.RateLimitUtils;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author Marcos Barbero
 * @author Liel Chayoun
 */
@RequiredArgsConstructor
public abstract class AbstractRateLimitFilter extends ZuulFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRateLimitFilter.class);

    private final RateLimitProperties properties;

    private final RouteLocator routeLocator;

    private final UrlPathHelper urlPathHelper;

    private final RateLimitKeyGenerator rateLimitKeyGenerator;

    private final RateLimitUtils rateLimitUtils;

    @Override
    public boolean shouldFilter() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        return properties.isEnabled() && !policy(route(request), request).isEmpty();
    }

    Route route(HttpServletRequest request) {
        String requestURI = urlPathHelper.getPathWithinApplication(request);
        LOGGER.info(" route: {}", requestURI);
        return routeLocator.getMatchingRoute(requestURI);
    }

    protected List<Policy> policy(Route route, HttpServletRequest request) {
        Map<String, Policy> policyMap = properties.getDefaultPolicyList().stream()
                .collect(Collectors.toMap(policy -> rateLimitKeyGenerator.key(request, route, policy), Function.identity()));
        if (route != null) {
            properties.getPolicies(route.getId()).forEach(policy ->
                    policyMap.put(rateLimitKeyGenerator.key(request, route, policy), policy));
        }
        return policyMap.values().stream()
                .filter(policy -> applyPolicy(request, route, policy))
                .collect(Collectors.toList());
    }

    private boolean applyPolicy(HttpServletRequest request, Route route, Policy policy) {
        List<MatchType> types = policy.getTypes();
        return types.isEmpty() || (urlApply(types, route) && ipApply(types, request) && userApply(types, request) && parApply(types, request));
    }

    private boolean userApply(List<MatchType> types, HttpServletRequest request) {
        List<String> users = getConfiguredType(types, Type.USER);
        return users.isEmpty()
                || users.contains(rateLimitUtils.getUser(request));
    }

    private boolean ipApply(List<MatchType> types, HttpServletRequest request) {
        List<String> ips = getConfiguredType(types, Type.IP);
        return ips.isEmpty()
                || ips.contains(rateLimitUtils.getRemoteIpAddress(request));
    }

    private boolean urlApply(List<MatchType> types, Route route) {
        List<String> urls = getConfiguredType(types, Type.URL);
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        return urls.isEmpty()
                || route == null
                || urls.stream().anyMatch(url -> antPathMatcher.match(url, route.getPath()));
    }

    private boolean parApply(List<MatchType> types, HttpServletRequest request) {
        List<String> pars = getConfiguredType(types, Type.PAR);
        return pars.isEmpty()
                || pars.stream().allMatch(par -> (!StringUtils.isEmpty(request.getParameter(par))));
    }

    private List<String> getConfiguredType(List<MatchType> types, Type user) {
        return types.stream()
                .filter(matchType -> matchType.getType().equals(user))
                .map(MatchType::getMatcher)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
}
